【Android TimeCat】 Android中使用矢量图(SVG, VectorDrawable)

背景

TimeCat项目中需要根据不同的场景动态改变图标颜色,如果用png等格式,会使图片资源过多。明明图案是一样的,为什么改变个颜色就得多一张图?如果整体颜色风格改变,那之前的图片资源就都废了?所以选择用xml文件来描述图片颜色,想用什么色就用什么色。

图片本质上是一个存像素点的矩阵,而svg高级一点,存一些点,比如一个圆,那么就存圆心和半径数据就行了,这是轨迹,然后再规定颜色,这样和png资源相比,内存大大减少,还容易自定义,改个图标颜色简直不要太方便!

SVG 和 VectorDrawable

SVG

可缩放矢量图形(英语:Scalable Vector Graphics,SVG)是一种基于可扩展标记语言(XML),用于描述二维矢量图形的图形格式。SVG由W3C制定,是一个开放标准。——摘自维基百科

.svg格式相对于.jpg.png甚至.webp具有较多优势,我认为核心有两点:

  • 省时间。图像与分辨率无关,收放自如,适配安卓机坑爹的分辨率真是一劳永逸;
  • 省空间。体积小,一般复杂图像也能在数KB搞定,图标更不在话下。

VectorDrawable

VectorDrawable是Google从Android 5.0开始引入的一个新的Drawable子类,能够加载矢量图。到现在通过support-library已经至少能适配到Android 4.0了(通过AppBrain统计的Android版本分布来看,Android 4.1以下(api<15)几乎可以不考虑了)。Android中的VectorDrawable只支持SVG的部分属性,相当于阉割版。

它虽然是个类,但是一般通过配置xml再设置到要使用的控件上。在Android工程中,在资源文件夹res/drawable/的目录下(没有则需新建),通过<vector></vector>标签描述,例如svg_ic_arrow_right.xml

1
2
3
4
5
6
7
8
9
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="8dp"
android:height="8dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
</vector>

基本属性说明:

  • width, height:图片的宽高。可手动修改到需要尺寸;
  • viewportHeight, viewportWidth:对应将上面height width等分的份数。以svg_ic_arrow_right.xml举例,可以想象将长宽都为8dp的正方形均分为24x24的网格,在这个网格中就可以很方便地描述点的坐标,图像就是这些点连接起来构成的。
  • fillColor:填充颜色。最好直接在这里写明色值#xxxxxxxx,而不要用@color/some_color的形式,避免某些5.0以下机型可能会报错。
  • pathData:在2中描述的网格中作画的路径。具体语法不是本文的重点,故不展开。

下面这段代码描述出来的是一个蓝色闹钟,可以从Android Studio的preview功能栏里预览到它的样子:

emm…既然xml资源作图标这么方便,应该怎么获取呢?

获取矢量图方式一:Android StudioMaterial Icon

鼠标选中drawable文件夹,右键, NewVector Asset

然后出现:

点击机器人进入搜索筛选:

左侧的搜索和分类可以快速索引。这里都是由谷歌官方制作的MD标准图标,建议先到这里搜索,如果没有再到网上搜索。

获取矢量图方式二:iconfont

墙裂安利一个网站,阿里的iconfont,海量在线矢量图,早收藏早致富!我已经离不开它了= ̄ω ̄=

第一步,搜索你要的资源名字,中英文一般都会有结果。比如“arrow”,结果:

第二步,鼠标移动到某一图标上点击,比如上面第一排第二个,出现:

三个选项,第一相当于购物车,可不用登录,第二是收藏,第三是下载,均需要登录。如果未登录,点击后出现:

选择GitHub或微博都行。
第三步,登录成功,点击下载,弹出:

可以对图标属性进行编辑,如色值和大小(单位dp),然后点按钮“SVG下载”。下载成功后在下载目录找到一个.svg格式的文件,这个文件可以用浏览器打开->查看网页源码,或者用NotePad++等编辑器打开看到里面的内容,格式化后是这样:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1490517024583" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1010" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
<defs>
<style type="text/css"></style>
</defs>
<path d="M288.86749 12.482601C272.260723-4.160867 245.369563-4.160867 228.720647 12.482601 212.15603 29.126068 212.15603 56.438425 228.720647 73.081892L704.289552 511.786622 228.720647 950.918109C212.15603 967.561574 212.15603 994.447175 228.720647 1011.517401 245.369563 1028.160866 272.260723 1028.160866 288.86749 1011.517401L794.952385 544.646802C803.803707 535.684935 807.597131 523.735776 807.007043 511.786622 807.597131 500.264224 803.803707 488.315065 794.952385 479.353198L288.86749 12.482601Z" p-id="1011"></path>
</svg>

文件里好多标签Android是不认识的。不过没关系,有三种解决办法

手动转化成xml

新建一个<vector></vector>标签的xml文件,通过观察文件内容,很容易获取到关键信息。

  • width, height自然对应<vector/>中宽高,
  • viewBox后两位数字是分别对应<vector/>中的viewportWidthviewportHeight
  • <path/>中的d的数据的对应<vector/><path/>中的pathData
  • fillColor自己手动设置。

svgtoandroid插件

安装:File -> Setting -> Plugins -> Browser repositories -> 搜“svg2VectorDrawable” -> 安装并重启Android Studio,再次进来后顶部工具栏会多一个图标:

点击图标弹出对话框:

勾选Batch选项,将对被选中文件夹中的.svg文件进行批量转换。nodpi会自动添加到没有后缀的drawable文件夹中。

网上下载的svg资源往往一步到位,有个这个插件将会事半功倍。导入第一个svg文件时就命名成我们想要的名字,如果不满意再导入时无需再关注命名,将后面导入的pathData覆盖第一个观察效果,直到满意后删除不需要的文件。

Android Studio自带转化

鼠标选中drawable文件夹,右键, New, Vector Asset, Local file,然后出现:

先选本地文件(还能支持PSD,强吧),再到磁盘中找到之前下载的.svg矢量图。导入后可以为文件重命名(建议用svg_或者有区别于其它格式的前缀),默认导入宽高均为24dp,选中Override框则读取文件本来宽高,其它配置视需求而定。点击Next到下一页最后点Finish就导入了。自动导入需要格式化一下就是前面svg_ic_arrow_right.xml的样子了。

海搜比较耗时间,线条粗细啦,位置没居中啦,大小不搭配啦,关键是这些问题都是导入项目或者运行到手机后才能发现(非强迫症当我没说)。
iconfont还有诸多成套的图标库,优点是风格大小一致,或者多彩图标。

项目应用

前提:

项目的build.gradle配置有:

1
2
3
4
5
6
7
8
9
10
11
12
13
android{
...
defaultConfig {
...
vectorDrawables.useSupportLibrary = true
}
...
}
dependencies {
...
compile "com.android.support:appcompat-v7:21+" // 至少Api21
...
}

项目的Activity中都包含(通用做法是在BaseActivity中加):

1
2
3
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}

AppCompatImageView

这是继承自ImageView用于5.0以下加载矢量图的控件,只需要替换src为srcCompat属性,其它没什么不同。例如:

1
2
3
4
<android.support.v7.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/svg_ic_arrow_right"/>

如果你的Activity直接或间接继承自AppCompatActivity,当前视图中的ImageView在编译过程中会被自动转为AppCompatImageViewsupport包中所有含有AppCompat前缀的控件均受相同处理),因而在Activity中通过findViewById()的实例用ImageViewAppCompatActivity接收是没有区别的。
用以上条件的Activity中装载的Fragment,或者通过动态注入(如DialogcontentView)的ImageView,均将被自动转为AppCompatActivity
xml文件中初始化ImageView并加载矢量图,必须使用AppCompatImageViewsrcCompat属性。
ImageView的染色属性tint同样适合矢量图。

TextView

在我的经验中,TextView可以用到矢量图的场景是最多的,主要是设置CompoundDrawable。例如:

1
2
3
4
5
6
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableRight="@drawable/svg_ic_arrow_right"
android:drawablePadding="4dp"
android:text="drawable right"/>

这样设置后,没有任何不适,编译器也不报错,可能你自己运行也没问题。但是!这才是深坑啊。5.0以下某些机型可能会崩溃的。

AppCompatTextView是没有对CompoundDrawable进行适配的,所以需要自己动手才能丰衣足食。简单原理是,判断系统版本如果小于5.0,就用ContextCompat.getDrawable获取到Drawable实例,再setCompoundDrawablesWithIntrinsicBounds

这个部分已经有人做好并开源了,地址:VectorCompatTextView,轻松compile到项目中使用。他还特意添加了一个实用功能——tint染色——可以选择是否让图标与文字颜色一样,这样就不必关心xml里的fillColor属性了。用例:

1
2
3
4
5
6
7
8
9
10
<com.xw.repo.VectorCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/color_gray_light"
android:gravity="center_vertical"
android:padding="16dp"
android:text="Next"
android:textSize="16sp"
app:drawableRightCompat="@drawable/svg_ic_arrow_right"
app:tintDrawableInTextColor="true"/>

效果:

MenuItem就是在res/menu/目录下通过xml配置的菜单,适用于NavigationViewmenu属性ActivityonCreateOptionsMenu()注入的选项菜单。

VectorDrawable 转 Bitmap

自定义View中也可以自由使用矢量图。
首先需要将VectorDrawable 转为 Bitmap,看码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Bitmap getBitmapFromVectorDrawable(Context context, int drawableId) {
Drawable drawable = ContextCompat.getDrawable(context, drawableId);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
drawable = (DrawableCompat.wrap(drawable)).mutate();
}

Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);

return bitmap;
}

执行以上方法获得一个Bitmap的实例(设为mVectorBitmap),然后再在ondraw()里根据你的需求画出bitmap

1
2
3
4
5
6
7
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
///
canvas.drawBitmap(mVectorBitmap, left, top, paint);
///
}

参考:

https://www.jianshu.com/p/0555b8c1d26a

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器